﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Text;
using System.Collections;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.IO;
using System.Reactive.Threading.Tasks;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Windows.Markup;

using alib;
using alib.file;
using alib.Character;
using alib.Enumerable;
using alib.Reflection;
using alib.Debugging;

using agree.schema;

namespace agree
{
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// File position information in a TDL file. I suppose it could be used for other file types as well.
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	[DebuggerDisplay("{ToString(),nq}")]
	public struct FilePos
	{
		public static readonly FilePos None = new FilePos(0, 0, 0, "");

		public FilePos(int offs, int line, int col, String file)
		{
			Debug.Assert(offs >= 0 && line >= 0 && col >= 0);
			this.offs = offs;
			this.line = line;
			this.col = col;
			this.file = file;
		}
		public int offs, line, col;
		public String file;

		public int Offset { get { return offs; } }
		public int Line { get { return line; } }
		public int Column { get { return col; } }
		public String File { get { return file; } }

		public static int operator -(FilePos a, FilePos b)
		{
			if (a.file != b.file)
				return 0;
			return a.offs - b.offs;
		}

		public override String ToString()
		{
			String _f;
			if (file == null)
				_f = "(none)";
			else
			{
				_f = file;
				var dirs = SysObj.Instance.DefaultDirectories;	// fix fix
				if (dirs != null)
					foreach (var dir in dirs)
					{
						var df = Path.GetFullPath(dir);
						if (file.StartsWith(df, StringComparison.InvariantCultureIgnoreCase))
						{
							_f = file.Substring(df.Length);
							break;
						}
					}
			}
			return String.Format("file:{0} line:{1} col:{2} (offset:{3})", _f, line, col, offs);
		}
	};


	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// 
	/// </summary>
	/// <remarks>
	/// regarding the appearance of 'IList' here: even though it's already inherited and implemented by our base class,
	/// if we don't actually re-mention it, we can't install an explicit implementation for IList.Add, which is the member
	/// that Xaml uses to understand heterogeneous collections.
	/// </remarks>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	[DebuggerDisplay("local:{local_items.Count} inherited:{inherited_items()._Count()}")]
	public class DirectoryPathSet : identity_base, IList<String>
	{
		public static FileInfo FindFileInfo(IIdentity parent, String fn)
		{
			var dps = (DirectoryPathSet)PP.DefaultDirectories.FindTracePropertyValue(parent);
			if (dps != null)
				fn = dps.Clean(fn);

			String use_file;
			FileInfo fi;

			if ((fi = new FileInfo(fn)).Exists)
				return fi;

			if (fn != (use_file = Path.GetFullPath(fn)) && (fi = new FileInfo(use_file)).Exists)
				return fi;

			if (dps != null && fn != (use_file = dps.FindGrammarFile(fn)) && use_file != null && (fi = new FileInfo(use_file)).Exists)
				return fi;

			return default(FileInfo);
		}

		public DirectoryPathSet(IIdentity prv)
			: base(prv)
		{
			local_items = new List<String>();
			UseCaseSensitiveFilenames = false;
			comparer = new _cmp(this);
		}

		readonly _cmp comparer;

		public bool UseCaseSensitiveFilenames { get; set; }

		sealed class _cmp : IEqualityComparer<String>
		{
			public _cmp(DirectoryPathSet dps) { this.dps = dps; }
			DirectoryPathSet dps;

			public bool Equals(String x, String y)
			{
				return x == y || StringComparer.Ordinal.Equals(dps.Clean(x), dps.Clean(y));
			}
			public int GetHashCode(String obj)
			{
				return dps.Clean(obj).GetHashCode();
			}
		}

		public String Clean(String s)
		{
			s = s.Trim().Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

			if (!UseCaseSensitiveFilenames)
				s = s.ToLowerInvariant();

			for (int i = s.Length - 1; i >= 2; --i)
				if (s[i] != Path.DirectorySeparatorChar)
				{
					s = s.Substring(0, i + 1);
					break;
				}
			return s;
		}

		public String FindGrammarFile(String s_file, String context_dir = null)
		{
			if (s_file == null || String.IsNullOrWhiteSpace(s_file))
				throw new ArgumentNullException();

			s_file = s_file.TrimEnd(Charset.dq);

			String s_try = Path.GetFullPath(s_file);
			if (File.Exists(s_try))
				return s_try;

			s_file = ValidateItem(s_file);
			bool f_modified = s_try != s_file;

#if USE_LIST
			DistinctList<String> order_to_try = new DistinctList<String>(dd.Comparer);
#endif

			String current_dir = ValidateItem(Environment.CurrentDirectory);
			Debug.Assert(Path.IsPathRooted(current_dir));

			if (!Path.IsPathRooted(s_file))
			{
#if USE_LIST
				order_to_try.Add(current_dir);
#else
				if (File.Exists(s_try = Path.Combine(current_dir, s_file)))
					return s_try;
#endif
			}
			else
			{
				if (f_modified)
				{
#if USE_LIST
					order_to_try.Add(Path.GetDirectoryName(s_file));
#else
					if (File.Exists(s_file))
						return s_file;
#endif
				}
				s_file = Path.GetFileName(s_file);
			}

			if (context_dir != null)
			{
				context_dir = ValidateItem(context_dir);
				if (Comparer.Equals(context_dir, current_dir))
					context_dir = null;
				else
				{
#if USE_LIST
					order_to_try.Add(context_dir);
					if (!Path.IsPathRooted(context_dir))
						order_to_try.Add(Path.Combine(current_dir, context_dir));
#else
					//if (File.Exists(s_try = Path.Combine(context_dir, s_file)))
					//    return s_try;
					//if (!Path.IsPathRooted(context_dir) && File.Exists(s_try = Path.Combine(current_dir, context_dir, s_file)))
					//    return s_try;
					if (Path.IsPathRooted(context_dir))
					{
						if (File.Exists(s_try = Path.Combine(context_dir, s_file)))
							return s_try;
						context_dir = null;
					}
					else if (File.Exists(s_try = Path.Combine(current_dir, context_dir, s_file)))
						return s_try;
				}
#endif
			}

			foreach (var dir in this)
			{
				Debug.Assert(dir == ValidateItem(dir));
#if USE_LIST
				order_to_try.Add(dir);
				if (!Path.IsPathRooted(dir))
					order_to_try.Add(Path.Combine(current_dir, dir));
#else
				if (dir == current_dir)
				{
					Debug.Assert(!File.Exists(Path.Combine(current_dir, s_file)));
					continue;
				}
				if (Path.IsPathRooted(dir))
				{
					if (File.Exists(s_try = Path.Combine(dir, s_file)))
						return s_try;
					if (context_dir != null)
					{
						Debug.Assert(Path.IsPathRooted(context_dir));
						if (File.Exists(s_try = Path.Combine(dir, context_dir, s_file)))
							return s_try;
					}
				}
				else if (File.Exists(s_try = Path.Combine(current_dir, dir, s_file)))
					return s_try;

#endif
			}
			return null;
		}

		public String FindBinaryFile(String s_file, String script_dir)
		{
			//s_file = s_file.Trim(Charset.dq);
			if (File.Exists(s_file))
				return s_file;

			if (Path.IsPathRooted(s_file))
				return null;

			String s2;
			if ((s2 = fb(s_file, script_dir)) != null)
				return s2;

			var _tmp = this;
			if (_tmp != null)
				foreach (var config_dir in _tmp)
					if ((s2 = fb(s_file, config_dir)) != null)
						return s2;

			return null;
		}
		String fb(String s_file, String _dir)
		{
			if (_dir != null)
			{
				String s2 = Path.Combine(_dir, s_file);
				if (File.Exists(s2))
					return s2;

				s2 = Path.Combine(_dir, "bin", Schema.rg_astats[0].BuildType, s_file);
				if (File.Exists(s2))
					return s2;
			}
			return null;
		}

		public String ValidateItem(String s)
		{
			s = Clean(s);

			if (s.IndexOfAny(Path.GetInvalidPathChars()) != -1)
				throw new Exception();

			return s;
		}

		public static void AddObjectHandler(DirectoryPathSet dps, Object obj)
		{
			throw new Exception();
		}

		List<String> local_items;
		public void MultiAdd(Object value)
		{
			var s = value as String;
			if (s == null)
			{
				s = (String)PP.Text.GetValue(value);
				if (s == null)
					throw new Exception();
			}
			Add(s);
		}

		IEnumerable<String> inherited_items()
		{
			IIdentity walk = this.Trace;
			if (walk != null)
			{
				while ((walk = walk.Trace) != null)
				{
					var _other = (DirectoryPathSet)PP.DefaultDirectories.GetValue(walk);
					if (_other != null)
						return _other;
				}
			}
			return alib.Collections.Collection<String>.None;
		}

		public void Add(String s)
		{
			local_items.Add(ValidateItem(s));
		}

		public int Count { get { return local_items.Count + inherited_items()._Count(); } }

		public String this[int index]
		{
			get
			{
				if (index < local_items.Count)
					return local_items[index];
				return inherited_items().ElementAt(index - local_items.Count);
			}
			set { throw not.impl; }
		}

		public void Clear() { local_items.Clear(); }

		public bool Contains(String item) { throw not.impl; }

		public void CopyTo(String[] array, int arrayIndex) { throw not.impl; }

		public int IndexOf(String item) { throw not.impl; }

		public void Insert(int index, String item) { throw not.impl; }

		public void RemoveAt(int index) { throw not.impl; }

		public bool IsReadOnly { get { return false; } }

		public bool Remove(String item) { throw not.impl; }

		public IEnumerator<String> GetEnumerator()
		{
			foreach (var fn in local_items)
				yield return fn;
			foreach (var fn in inherited_items())
				yield return fn;
		}

		IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
	};


	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// 
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	public class ReadFile : _monad<String>,
		ρρText,
		ρρFilename
	{
		public ReadFile(IRtParent parent, String filename)
			: base(parent, filename)
		{
		}
		public ReadFile(IRtParent parent)
			: base(parent)
		{
		}

		protected Task<String> pre_fetch;

		protected override void ValidateArg0(ref String fn)
		{
			base.ValidateArg0(ref fn);

			Debug.Assert(pre_fetch == null);

			var fi = DirectoryPathSet.FindFileInfo(parent, fn);
			if (fi == null)
				throw new FileNotFoundException(String.Format("ReadFile XAML markup extension could not find the file '{0}'.", fn), fn);

			fn = fi.FullName;

			this.pre_fetch = _file_ext.ReadAsync(fi);
		}

		public String Filename
		{
			get { return base.t0; }
			set { base.t0 = value; }
		}

		[DebuggerBrowsable(DebuggerBrowsableState.Never)]
		public String Text
		{
			get { return pre_fetch.Result; }
			set { throw not.impl; }
		}
	};

	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	/// <summary>
	/// 
	/// </summary>
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	public class ReadFileLines : ReadFile
	{
		public ReadFileLines(IRtParent parent, String filename)
			: base(parent, filename)
		{
		}
		public ReadFileLines(IRtParent parent)
			: base(parent)
		{
		}

		public override Object ProvideValue(IServiceProvider sp)
		{
			var arr = pre_fetch.Result
				.Split(alib.Character.Charset.cr_lf, StringSplitOptions.RemoveEmptyEntries)
				.Select(s => s.Trim())
				.Where(s => s.Length > 0 && s[0] != ';')
				.ToArray();
			return arr;
		}
	};
}
